/** * Grist Functions * * Requires jQuery, jquery.qtoggle, 'grist-universal-js' * Copyright (c) 2009-2112 Grist (grist.org) */ var GRIST = GRIST || {}; jQuery(function(){ // Nav dropdown functionality GRIST.NAV.init(); // Allow vertical share tools to float on scroll GRIST.vertical_share_tools(); // support subscribing to email via ajax GRIST.EMAIL_SUB.init('.email-subscription-form'); // Controller for overlays GRIST.OVERLAY_QUEUE.init(); // support for lightboxing arbitrary inline HTML // @see http://www.jacklmoore.com/colorbox/example1/ example 'Inline HTML' if( typeof jQuery.colorbox === 'function' ) jQuery('a.lightbox-inline-content').colorbox( { inline: true } ); // add qToggle listeners on article content and email subscription forms jQuery( '.article-body, .email-subscription-form' ).qToggle(); // for content that should only be hidden when js is available // useful when you want toggle content to show for non-js clients jQuery( '.jshide' ).hide(); // Hide email subscribe widget for subscribers if ( GRIST.READER.is_subscriber_email ) jQuery('#widget-subscribe').hide(); }); // DOM ready; /*----------------------------------------------------------------------------*/ /** * Controller that determines which lightbox to show * * If an overlay deems that it should be shown, it enqueues itself as follows * * GRIST.OVERLAY_QUEUE.enqueue( 'overlay_name', overlay_object ); * * The overlay object must have a 'launch' method which opens the lightbox * with the appropriate settings and content. * * At the bottom of footer.php, after all overlays have had a chance * to enqueue themselves, we run process_queue() to decide which should be shown */ GRIST.OVERLAY_QUEUE = { init: function(){ // Set up the empty queue this.q = {}; }, /** * Add an overlay to the queue * Overlays must have a name and a method named 'launch' */ enqueue: function( name, overlay_obj ){ if( typeof name === 'undefined' || typeof overlay_obj === 'undefined' || ! overlay_obj.hasOwnProperty('launch') ) return; this.q[name] = overlay_obj; }, /** * Decides what overlay to show * Never show overlays to people with cookies or localStorage disabled * Never show overlays to people who have seen one during this visit already */ process_queue: function(){ // If they don't have cookies enabled, bail if( typeof jQuery.cookie !== 'function' || ! GRIST.HELPERS.are_cookies_enabled ) return; // If there's no localStorage, bail if( ! GRIST.STORAGE.is_enabled ) return; var visit_events = GRIST.STORAGE.get('visit_events'); // If there is no local storage key 'visit_events', bail if( jQuery.type( visit_events ) !== 'array' ) return; var visit_event = 'saw-overlay'; // If user has already seen an overlay during this visit, bail if( jQuery.inArray( visit_event, visit_events ) !== -1 ) return; // If we've made it this far, decide which overlay to show var q = this.q, overlays_by_priority = [ 'email_lightbox', 'appeal', 'survey' ]; jQuery.each( overlays_by_priority, function(i, overlay){ if( q.hasOwnProperty( overlay ) && q[overlay].hasOwnProperty('launch') ){ // launch the prioritized overlay q[overlay].launch(); // Mark user as having seen an overlay already this visit GRIST.STORAGE.add_to_array( 'visit_events', [ visit_event ] ); // break out of loop return false; } }); } // process_queue }; // GRIST.OVERYLAY_QUEUE /*----------------------------------------------------------------------------*/ /* Support for vertical share tools on author-blogs -- set to fixed postion when scrolling page up */ GRIST.vertical_share_tools = function(){ var $toolbar = jQuery('#floating-share-tools'); if ( ! $toolbar.length ) return; // find the top of the floating share tools var tools_top = $toolbar.offset().top - parseFloat( $toolbar.css('margin-top').replace(/auto/, 0) ); // toggle class that fixes share tools for wider clients jQuery(window).scroll( function () { var y = jQuery(this).scrollTop() + 5; if ( y >= tools_top ) $toolbar.addClass('fixed-vertical-bar'); else $toolbar.removeClass('fixed-vertical-bar'); }); }; // GRIST.vertical_share_tools /*----------------------------------------------------------------------------*/ /** * Dropdown functionality for main nav */ GRIST.NAV = { init: function(){ var _this = this; // Lower, hidden part of the nav this.$nav_lower = jQuery('#main-nav-lower'); //our dropdownable menus this.$nav_dropdowns = jQuery(".main-nav-lvl-1 .dropdown"); //setup behaviors on each dropdown parent this.$nav_dropdowns.each( function( index, dropdown ) { //get the dropdown container and it's link var $dropdown = jQuery( dropdown ), $dropdown_link = $dropdown.find( 'a:first' ); //set link click behavior $dropdown_link.on( 'click.grist_nav', function( e ){ e.preventDefault(); //if it's already showing, close it if( $dropdown.hasClass( 'main-nav-active' ) ){ _this.close_submenu_container(); _this.deactivate_dropdown_tab( $dropdown ); } else { // otherwise, show the submenu //deactivate all other tabs _this.deactivate_all_nav_tabs(); //hightlight the clicked tab _this.activate_dropdown_tab( $dropdown ); //inject appropriate content _this.populate_submenu_container( $dropdown ); //open container _this.open_submenu_container(); } }); //end dropdown link click }); //end each dropdown }, //init /** * Injects a particular submenu into a container for showing * @param $dropdown An li which contains the submenu to drop down */ populate_submenu_container: function( $dropdown ){ var $subnav = $dropdown.find('.main-nav-lvl-2'); //clear out old content this.$nav_lower.empty(); //inject subnav $subnav .clone() .appendTo( this.$nav_lower ); //add close arrow this.add_close_arrow_to_submenu( this.$nav_lower ); }, // populate_submenu_container close_submenu_container: function(){ this.$nav_lower.hide(); }, open_submenu_container: function(){ this.$nav_lower.show(); }, /** * Style parent link to indicate that dropdown is inactive * @param jQuery Object $dropdown An li which contains the submenu to drop down */ deactivate_dropdown_tab: function( $dropdown ){ $dropdown //controls coloring of tab .removeClass('main-nav-active') //changes arrow direction .find('.dropdown-arrow').removeClass('sprite-small-up-arrow').addClass('sprite-small-down-arrow'); }, /** * Style parent link to indicate it's the parent of the subnav * @param jQuery Object $dropdown An li which contains the submenu to drop down */ activate_dropdown_tab: function( $dropdown ){ $dropdown //controls coloring of tab .addClass('main-nav-active') //changes arrow direction .find('.dropdown-arrow').removeClass('sprite-small-down-arrow').addClass('sprite-small-up-arrow'); }, /** * Inject an arrow into the subnav that will close it * @param jQuery Object */ add_close_arrow_to_submenu: function( $subnmenu_container ){ // For scoping problems inside click handler var _this = this; //create close arrow $close_arrow = jQuery(''); //setup close arrow behaviors $close_arrow.on( 'click.grist_nav', function( e ){ e.preventDefault(); _this.deactivate_all_nav_tabs(); _this.close_submenu_container(); }); //end close_arrow.click // inject close arrow $subnmenu_container.append( $close_arrow ); }, /** * Style all dropdown parent links to indicate they are inactive */ deactivate_all_nav_tabs: function(){ var _this = this; //deactivate all tabs this.$nav_dropdowns.each( function( index, menu ){ _this.deactivate_dropdown_tab( jQuery( menu ) ); }); } }; // GRIST.NAV /*----------------------------------------------------------------------------*/ /** * Listens for submit events coming from forms with a * certain selector and sends a subscription request to * WhatCounts via AJAX. * * If the subscription is successful, fires a custom "grist_subscribe" event. * * The 'this' keyword always refers to GRIST.EMAIL_SUB through clever use of * jQuery.proxy */ GRIST.EMAIL_SUB = { /** * Sets the submit event listener * @param str selector css selector that identifies the subscribe forms * that should be listened to */ init: function( selector ){ if( ! selector || typeof selector !== 'string' ) return; this.subscribe_url = '/wp-admin/admin-ajax.php'; this.generic_success_str = 'Thanks! You\'re subscribed!'; this.generic_error_str = 'Oops! Something went wrong subscribing you. Please try again later.'; jQuery('body').on( 'submit.grist.email_sub', selector, jQuery.proxy( this.submit_handler, this ) ); }, // init /** * Displays loading message and makes an ajax call to our subscription API * @param event e Form submit event */ submit_handler: function(e){ e.preventDefault(); this.$form = jQuery(e.target); // Disable submit button so people can't multi-submit this.$form.find('input[type="submit"], button[type="submit"]').attr('disabled','disabled'); // Clear alerts this.$form.find('.alert').remove(); // Display loading message this.$form.append( this.get_alert_html('Loading...') ); var ajax_params = { type: "POST", url: this.subscribe_url, data: this.$form.serialize(), dataType: 'json' }; // Make ajax call and set callbacks jQuery.ajax( ajax_params ) .done( jQuery.proxy( this.process_sub_results, this ) ) .fail( jQuery.proxy( this.set_generic_error, this ) ) .always( jQuery.proxy( this.display_sub_results, this ) ); }, /** * Once ajax call is complete, set the message that will be displayed * If the subscription was a success, announce it. * @param object data JSON response * @param string textStatus the status of the ajax response */ process_sub_results: function( data, textStatus ){ // Was the subscription successful? if( this.is_sub_success( data ) ){ // Announce the subscription so other plugins can react GRIST.SUBSCRIPTION_TRACKING.announce_subscribe( this.get_subscription_data() ); // Set the success message this.result_html( this.get_alert_html( this.generic_success_str, 'alert-success') ); } else { // Set an appropriate error message var error_msg = this.get_whatcounts_error( data ); if( error_msg ) this.result_html( this.get_alert_html( error_msg, 'alert-error') ); else this.set_generic_error(); } }, /** * Displays the appropriate message based on subscription result (error or success) */ display_sub_results: function(){ var _this = this; // Display results after a delay setTimeout(function(){ // Re-enabled submit button _this.$form.find('input[type="submit"], button[type="submit"]').removeAttr('disabled'); // Clear alerts _this.$form.find('.alert').remove(); // Display result _this.$form.append( _this.result_html() ); }, 300); }, /** * Getter and setter for the result message html * @param string html Markup for the result message * @return str|void If html is supplied, returns nothing, if not, it returns the message html. */ result_html: function( html ){ if( typeof html !== 'undefined' ) this.result_msg = html; else if( this.hasOwnProperty('result_msg') ) return this.result_msg; else return ''; }, /** * Did the subscription succeed? * @param object JSON response from subscription attempt * @return bool */ is_sub_success: function( response_data ){ if( response_data.hasOwnProperty('ACK') && response_data.ACK === "SUCCESS" ) return true; else return false; }, /** * Builts the markup of an alert * @param string msg Text of the message * @param string alert_class CSS class for the alert, i.e. 'alert-error', 'alert-success' * @return string Alert markup */ get_alert_html: function( msg, alert_class ){ if( typeof alert_class !== 'string' ) alert_class = ''; return "
" + msg + "
"; }, /** * Get the error message from the WhatCounts JSON response * @param object response_data JSON response from subscription attempt * @return str | bool Error message or false if none */ get_whatcounts_error: function( response_data ){ if( response_data.hasOwnProperty('ERROR') ) return response_data.ERROR; else return false; }, /** * Sets a generic error to be displayed. * To be used when we don't know what went wrong. */ set_generic_error: function(){ this.result_html( this.get_alert_html( this.generic_error_str, 'alert-error' ) ); }, /** * Extracts the data about a successful subscription such as * email address, subscriptions, location of the form used to subscribe * * @return object */ get_subscription_data: function(){ // Get the subscription data var email = this.$form.find('input[name="sub_email"]').val(), form_id = this.$form.attr('id'), form_location = form_id.replace('-email-subscription-form', ''), lists = []; // Get all the newsletter subscriptions this.$form.find('[name="sub_lists[]"]').each(function(i, list_input){ lists.push( jQuery(list_input).val() ); }); // Package it up var subscription_data = { sub_type: 'email', subs: lists, sub_email: email, sub_location: form_location }; return subscription_data; } // get_subscription_data }; // GRIST.EMAIL_SUB ; /* * Causes the offscreen div #grist-flyout-box * Use location hash #debug-flyout to throw into debug mode to ignore cookies * @todo make this a jq plugin we can instatiate with supplied object for flyout content */ // @requires jQuery.fn.cookie https://github.com/carhartl/jquery-cookie jQuery(function($){ // settings var active_pages_selector = '#article-page,#grist-list-page,#gristmill-page,#slideshow-page'; // selector on pages that should have a flyout, req var $trigger_element = jQuery('#grist-end-of-article'); // flyout shows when this comes into view, req var cookie_session_name = 'grist_flyout_session'; var cookie_count_name = 'grist_flyout_count'; var cookie_session_options = { expires:1, path:'/' }; // don't set expires key to make a session cookie var cookie_count_options = { expires:30, path:'/' }; var max_views = 3; // how many sequential sessions we will show the flyout to the user var flyout_width = 420; // how far are we sliding in and out? var debug = window.location.hash == '#debug-flyout' || false; var flyout_id = '#grist-flyout-box'; // run conditions: // // check dependencies if( 'function' !== typeof( $.cookie ) ) return; // escape if on the wrong page if( ! $(active_pages_selector).length ) return; // don't flyout if cookies are disabled if ( ! GRIST.HELPERS.are_cookies_enabled ) return; // escape if we are a prior subscriber if( GRIST.READER.is_subscriber_email && ! debug ) return; // only show the flyout once per session if( $.cookie( cookie_session_name ) && ! debug ) return; // only show the first three sessions var grist_flyout_count = parseInt( $.cookie( cookie_count_name ), 10 ) || 0; if( grist_flyout_count >= max_views && ! debug ) return; // track flyout state in this session var grist_flyout_closed = false; var grist_flyout_hidden = true; // scroll event polling @see http://ejohn.org/blog/learning-from-twitter/ var didScroll = false; $(window).on('scroll.grist_flyout', null, null, function(){ didScroll = true; }); var flyout_scroll_interval = setInterval(function() { if ( didScroll ) { didScroll = false; if( GRIST.HELPERS.is_el_on_screen( $trigger_element ) ) grist_flyout_open(); } }, 250); function grist_flyout_open(){ $(flyout_id).stop().animate( {'right':'0'}, 400, 'swing', function(){ grist_flyout_set_cookies(); grist_flyout_disable(); grist_flyout_hidden = false; _kmq.push(['record', 'flyout-seen']); }); } function grist_flyout_close(){ $(flyout_id).stop().animate( {'right':'-' + flyout_width + 'px'}, 400, 'swing', function(){ $('#grist-flyout-close').unbind('click.grist_flyout.close'); $('#grist-flyout-mark-subscribed').unbind('click.grist_flyout.mark_subscribed'); grist_flyout_disable(); grist_flyout_hidden = true; }); } function grist_flyout_set_cookies(){ $.cookie( cookie_session_name, 1, cookie_session_options ); // we saw the flyout this session $.cookie( cookie_count_name, grist_flyout_count + 1, cookie_count_options ); // increament number of views } /** * let's free up some resources. */ function grist_flyout_disable(){ $(window).unbind('scroll.grist_flyout'); clearInterval( flyout_scroll_interval ); } // click to close flyout behavior $("#grist-flyout-close").on('click.grist_flyout.close', grist_flyout_close ); // hide the subscribe flyout if the user says the are subscribed // Give them a message about not bothering them again // mark them as a subscriber too if they aren't already $('#grist-flyout-mark-subscribed').on('click.grist_flyout.mark_subscribed', function(e){ e.preventDefault(); $('#grist-flyout-box .grist-flyout-excerpt').html('

Thanks! We\'ll stop bugging you. :)

'); GRIST.READER.add_subscriptions(['email-daily']); setTimeout( function(){ $('#grist-flyout-close').trigger('click'); return false; }, 2500 ); }); }); ; /** * A lightbox shown to visitors from whitelisted sources to encourage them to sign up * for the daily newsletter. * * @requires lightbox, grist-universal * * If this overlay thinks it should be shown, it should enqueue itself as follows: * * GRIST.OVERLAY_QUEUE.enqueue( 'overlay_name', overlay_object ); * * If multiple overlays want to be shown, this queue will decide which one to show. * * An overlay object must have a 'launch' method which opens the lightbox * with the appropriate settings and content. * */ var GRIST = GRIST || {}; jQuery(function(){ // The lightbox will be shown to any non-subscribers // with a utm_source value in this array. var source_whitelist = [ 'outbrain' ]; GRIST.EMAIL_LIGHTBOX.init( source_whitelist ); }); GRIST.EMAIL_LIGHTBOX = { /** * Initialize email lightbox * @param {array} source_whitelist array of utm_source values to show this lightbox to */ init: function( source_whitelist ){ this.name = 'email_lightbox'; this.source = 'unknown'; this.whitelisted_sources = []; // Incorporate the provided source whitelist if( typeof source_whitelist !== 'undefined' ) this.whitelisted_sources = source_whitelist; // Requires lightbox if( typeof jQuery.colorbox === 'undefined' ) return; // Requires the overlay queue if( ! GRIST.hasOwnProperty('OVERLAY_QUEUE') ) return; // Determine where the user is coming from this.determine_source(); if( ! this.should_be_shown() ) return; // update the sub_source to include the source name var $input_source = jQuery(".lightbox-email-subscription-form-container").find("input[name='sub_source']"); $input_source.val( $input_source.val() + '_' + this.source ); // Add the overlay to the queue for consideration GRIST.OVERLAY_QUEUE.enqueue( this.name, this ); }, /** * Displays the lightbox */ launch: function(){ var $container = jQuery('#email-lightbox-content'), html_content = $container.html(), _this = this; // Get rid of the footer markup so we don't double // up on IDs $container.remove(); var colorbox_params = { html: html_content, transition: 'none', scrolling: false, onComplete: function(){ _this.set_event_listeners(); _kmq.push(['record', _this.source + '-email-lightbox-view']); //@hack: this lightbox resizes during interaction //and we don't want the black background showing through jQuery('#cboxLoadedContent').css('background-color', 'transparent'); } }; jQuery.colorbox(colorbox_params); }, // launch /** * Resizes the lightbox to fix the loading/success/error message and closes * the lightbox on successful subscribe */ set_event_listeners: function(){ var _this = this; var $body = jQuery('body'); var $ask_container = jQuery('.lightbox-email-subscription-ask-container'); var $form_container = jQuery('.lightbox-email-subscription-form-container'); // React to user's 'agree or disagree' $ask_container.find('button').on('click.email-lightbox', function(e){ e.preventDefault(); var $button = jQuery(this); // If the user agrees, then show them an email form, // if not, then close the lightbox if( $button.hasClass('agree') ){ _kmq.push(['record', _this.source + '-email-lightbox-agree']); $ask_container.fadeOut(function(){ $form_container.fadeIn(function(){ jQuery.colorbox.resize(); }); }); } else { _kmq.push(['record', _this.source + '-email-lightbox-disagree']); jQuery.colorbox.close(); } }); // Resize lightbox on submit $form_container.find('form').on('submit.email-lightbox', function(e){ // @hack: I am delaying this so loading message html is sure to be // injected before the resize; setTimeout( function(){ jQuery.colorbox.resize(); }, 10); }); // Close lightbox on successful subscribe $body.on('grist_subscribe.email-lightbox', function( e, data ){ // If this wasn't a lightbox subscribe, ignore if( typeof data !== 'object' || ! data.hasOwnProperty('sub_location') || data.sub_location !== 'lightbox' ) return; _kmq.push(['record', _this.source + '-email-lightbox-subscribe']); setTimeout(function(){ jQuery.colorbox.close(); }, 700); }); }, /** * Determine's the the source of the user from the utm_source param */ determine_source: function(){ // Get the utm_source URL parameter var source = GRIST.HELPERS.get_URL_param('utm_source'); // Set lightbox source if( source ) this.source = source; }, /** * Should the lightbox be shown? Right now it is shown to anyone * coming from a whitelisted source who is not already subscribed * * @return bool */ should_be_shown: function(){ // If they're already subscribed, don't show if( GRIST.READER.is_subscriber_email ) return false; // If they're not from a whitelisted source, don't show if( jQuery.inArray( this.source, this.whitelisted_sources ) === -1 ) return false; return true; } }; // GRIST.EMAIL_LIGHTBOX; /** * An overlay displaying a link to a survey * * @requires lightbox, grist-universal * * If this overlay thinks it should be shown, it should enqueue itself as follows: * * GRIST.OVERLAY_QUEUE.enqueue( 'overlay_name', overlay_object ); * * If multiple overlays want to be shown, this queue will decide which one to show. * * An overlay object must have a 'launch' method which opens the lightbox * with the appropriate settings and content. */ var GRIST = GRIST || {}; jQuery(function(){ GRIST.SURVEY.init(); }); GRIST.SURVEY = { init: function(){ this.name = 'survey'; // Requires lightbox if( typeof jQuery.colorbox === 'undefined' ) return; // Requires the overlay queue if( ! GRIST.hasOwnProperty('OVERLAY_QUEUE') ) return; // Requires local storage if( ! GRIST.STORAGE.is_enabled ) return; this.survey_url = jQuery('#grist-survey-link').attr('href'); if( ! this.should_be_shown() ) return; // Add the overlay to the queue for consideration GRIST.OVERLAY_QUEUE.enqueue( this.name, this ); }, // init /** * Displays the lightbox */ launch: function(){ var $container = jQuery('#grist-survey-lightbox-container'), html_content = $container.html(), _this = this; $container.remove(); var colorbox_params = { html: html_content, transition: 'none', scrolling: false, onComplete: function(){ // Mark survey as seen GRIST.STORAGE.add_to_array('survey_prompts_seen', [ _this.survey_url ]); // Listen for button clicks _this.set_event_listeners(); // Record view _kmq.push(['record', 'grist-survey-lightbox-view']); } }; jQuery.colorbox(colorbox_params); }, // launch /** * Closes the lightbox when a button is pressed, fires analytics events */ set_event_listeners: function(){ $container = jQuery('.grist-survey-container'); // React to user's 'agree or disagree' $container.find('.btn').on('click.outbrain-email-lightbox', function(e){ var $button = jQuery(this); // If the user agrees, then show them an email form, // if not, then close the lightbox if( $button.hasClass('agree') ) _kmq.push(['record', 'grist-survey-agree']); else if( $button.hasClass('disagree') ) _kmq.push(['record', 'grist-survey-disagree']); jQuery.colorbox.close(); }); }, // set_event_listeners /** * Show the overlay to people who haven't seen it before and who * have been to the site at least twice ever * @return bool */ should_be_shown: function(){ // Has the user seen this survey? var surveys_seen = GRIST.STORAGE.get('survey_prompts_seen'), has_survey_been_seen = true; // If there is no local storage key 'survey_prompts_seen', means // we haven't shown any prompts yet if( jQuery.type( surveys_seen ) !== 'array' ) has_survey_been_seen = false; // If it's not in the array, we haven't seen it yet if( jQuery.inArray( this.survey_url, surveys_seen ) === -1 ) has_survey_been_seen = false; return GRIST.READER.visits > 1 && ! has_survey_been_seen; } // should_be_shown }; // GRIST.SURVEY;